use crate::{
    action::user::UserIdExt,
    fairing::user::UserDb,
    model::user::{UserData, UserId},
    token::{verify_token, TokenVerifyStatus},
};
use rocket::http::Status;
use rocket::outcome::try_outcome;
use rocket::request::{FromRequest, Outcome, Request};
use rocket_db_pools::{
    sqlx::{self, Acquire},
    Connection,
};
use std::fmt;

#[derive(Debug, Clone)]
pub enum UserAuthError {
    NoToken,
    BadHeader,
    BadToken,
    ExpiredToken,
    TokenTimeInvalid,
    UnknownUser,
    InternalError,
    NotAdminUser,
}

#[allow(clippy::enum_variant_names)]
#[derive(Debug)]
pub enum TokenExtractError {
    MissingAuthorizationHeader,
    InvalidAuthorizationHeader,
    MultipleAuthorizationHeader,
}

impl fmt::Display for TokenExtractError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::MissingAuthorizationHeader => write!(f, "No authorization header found"),
            Self::InvalidAuthorizationHeader => write!(f, "Invalid authorization header"),
            Self::MultipleAuthorizationHeader => write!(f, "Found multiple authorization header"),
        }
    }
}

impl std::error::Error for TokenExtractError {}

fn extract_token<'r>(req: &'r Request<'_>) -> Result<&'r str, TokenExtractError> {
    let headers: Vec<_> = req.headers().get("Authorization").collect();

    let header = match headers.len() {
        0 => return Err(TokenExtractError::MissingAuthorizationHeader),
        1 => headers.first(),
        _ => return Err(TokenExtractError::MultipleAuthorizationHeader),
    };

    let value = match header {
        Some(val) => val,
        None => return Err(TokenExtractError::InvalidAuthorizationHeader),
    };

    let token = value.strip_prefix("Bearer ");
    match token {
        Some(token) => Ok(token),
        None => Err(TokenExtractError::InvalidAuthorizationHeader),
    }
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for UserId {
    type Error = UserAuthError;
    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let token_result = req
            .local_cache_async(async {
                match extract_token(req) {
                    Ok(token) => match verify_token(token).await {
                        TokenVerifyStatus::Verified(info) => Ok(info),
                        TokenVerifyStatus::Expired => {
                            Err((Status::Unauthorized, UserAuthError::ExpiredToken))
                        }
                        TokenVerifyStatus::Invalid => {
                            Err((Status::Unauthorized, UserAuthError::BadToken))
                        }
                        TokenVerifyStatus::TimeInvalid => {
                            Err((Status::Unauthorized, UserAuthError::TokenTimeInvalid))
                        }
                    },
                    Err(err) => match err {
                        TokenExtractError::MissingAuthorizationHeader => {
                            Err((Status::Unauthorized, UserAuthError::NoToken))
                        }
                        TokenExtractError::InvalidAuthorizationHeader
                        | TokenExtractError::MultipleAuthorizationHeader => {
                            Err((Status::BadRequest, UserAuthError::BadHeader))
                        }
                    },
                }
            })
            .await;

        let token_info = match token_result.as_ref() {
            Ok(info) => info.clone(),
            Err(err) => return Outcome::Error((*err).clone()),
        };

        Outcome::Success(token_info.user_id)
    }
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for UserData {
    type Error = UserAuthError;
    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let user_identifier = try_outcome!(req.guard::<UserId>().await);
        let mut db;
        match req.guard::<Connection<UserDb>>().await {
            Outcome::Success(user_db) => db = user_db,
            Outcome::Forward(forward) => return Outcome::Forward(forward),
            Outcome::Error(err) => return Outcome::Error((err.0, UserAuthError::InternalError)),
        };
        let result = async {
            let mut tx = db.begin().await?;

            let info = user_identifier.get_user(&mut tx).await?;

            tx.commit().await?;
            Ok::<Option<UserData>, sqlx::Error>(info)
        }
        .await;
        match result {
            Ok(info) => match info {
                Some(user) => Outcome::Success(user),
                None => Outcome::Error((Status::Unauthorized, UserAuthError::UnknownUser)),
            },
            Err(_) => Outcome::Error((Status::InternalServerError, UserAuthError::InternalError)),
        }
    }
}

#[allow(dead_code)]
pub struct AdminUserData(UserData);

#[rocket::async_trait]
impl<'r> FromRequest<'r> for AdminUserData {
    type Error = UserAuthError;
    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        let user_data = try_outcome!(req.guard::<UserData>().await);
        if user_data.info.is_admin {
            Outcome::Success(AdminUserData(user_data))
        } else {
            Outcome::Error((Status::Forbidden, UserAuthError::NotAdminUser))
        }
    }
}
